// src/services/BackupService.js
/**
 * 备份服务模块
 * 用于实现数据库的完整备份与增量备份功能
 * 支持备份文件加密、分片处理大数据量和自动清理过期备份
 */

// 导入必要的依赖库
const mysqldump = require('mysqldump'); // MySQL数据库备份工具
const fs = require('fs-extra');         // 增强版文件系统操作库
const path = require('path');           // 路径处理工具
const moment = require('moment-timezone'); // 日期时间处理库(带时区)
const crypto = require('crypto');       // 加密功能库
const prisma = require('../config/database'); // Prisma ORM客户端
const formatDateTime = require('../utils/formatDateTime');
require('dotenv').config();            // 加载环境变量配置

/**
 * 备份服务类
 * 处理系统数据的备份和还原相关功能
 */
class BackupService {
  /**
   * 构造函数
   * 初始化备份目录结构和配置参数
   */
  constructor() {
    // 创建备份目录结构
    this.backupDir = path.join(process.cwd(), 'backups'); // 主备份目录
    fs.ensureDirSync(this.backupDir); // 确保主备份目录存在
    
    // 创建各类型备份子目录
    this.fullBackupDir = path.join(this.backupDir, 'full'); // 全量备份目录
    this.incrementalBackupDir = path.join(this.backupDir, 'incremental'); // 增量备份目录
    this.differentialBackupDir = path.join(this.backupDir, 'differential'); // 差异备份目录
    
    // 确保各子目录存在
    fs.ensureDirSync(this.fullBackupDir);
    fs.ensureDirSync(this.incrementalBackupDir);
    fs.ensureDirSync(this.differentialBackupDir);
    
    // 备份和压缩选项
    this.defaultCompressionLevel = 9; // 最高压缩比，最慢但体积最小，适用于存储空间敏感场景（如备份）
    this.defaultEncryptionAlgorithm = 'aes-256-cbc'; // 默认加密算法，AES-256-CBC是一种强加密标准
    this.sliceSize = 5000; // 每个分片包含的试卷数量，防止单个文件过大
  }
  
  /**
   * 获取最近成功的全量备份时间
   * 用于差异备份和增量备份的起始时间点确定
   * @returns {Date|null} 最近成功全量备份的时间，如果不存在则返回null
   */
  async getLastFullBackupTime() {
    // 查询最近一次成功的全量备份记录
    const lastFullBackup = await prisma.backup_record.findFirst({
      where: {
        backup_type: 'full', // 只查找全量备份
        status: 'success'    // 且备份状态为成功的
      },
      orderBy: {
        created_at: 'desc'   // 按创建时间倒序排列，获取最新的
      }
    });
    
    // 返回备份时间，如果不存在则返回null
    return lastFullBackup ? lastFullBackup.created_at : null;
  }
  
  /**
   * 获取增量备份的时间范围
   * 确定本次增量备份应该涵盖的数据变更时间段
   * @returns {Object} 包含起始时间和结束时间的对象
   */
  async getIncrementalTimeRange() {
    // 找到最近的成功备份（无论类型）作为增量备份的起点
    const lastBackup = await prisma.backup_record.findFirst({
      where: {
        status: 'success' // 任何类型的成功备份
      },
      orderBy: {
        created_at: 'desc' // 按创建时间倒序排列，获取最新的
      }
    });
    
    // 设置时间范围：从上次备份时间到当前时间
    const startTimeDate = lastBackup ? lastBackup.created_at : null;
    // 格式化开始时间（用于SQL查询和显示）
    const startTime = startTimeDate ? formatDateTime(startTimeDate) : null;

   // 获取当前时间Date对象（用于Prisma查询）
    const now = new Date();
    const endTimeDate = new Date(now.getTime() + (8 * 60 * 60 * 1000));
    
    // 格式化结束时间（用于SQL查询和显示）
    const endTime = formatDateTime(endTimeDate);

    return {     
      startTime,    // 格式化的字符串
      endTime,      // 格式化的字符串
      startTimeDate, // 原始日期对象
      endTimeDate   // 原始日期对象 };
  }
}
  
  /**
   * 创建带密码的加密压缩文件
   * 将源文件或目录压缩并加密，增强备份数据安全性
   * @param {string} sourcePath 源文件或目录路径
   * @param {string} destPath 目标压缩文件路径
   * @param {string} password 加密密码
   * @returns {Promise<Object>} 返回包含路径、大小和IV的对象
   */
  
  async createEncryptedArchive(sourcePath, destPath, password) {
    return new Promise((resolve, reject) => {
      try {
        // 导入所需模块
        const archiver = require('archiver');
        const fs = require('fs-extra');
        
        // 注册加密格式（全局只需注册一次）
        if (!this.isZipEncryptedRegistered) {
          archiver.registerFormat('zip-encrypted', require('archiver-zip-encrypted'));
          this.isZipEncryptedRegistered = true;
        }
        
        // 创建输出流
        const output = fs.createWriteStream(destPath);
        
        // 创建支持密码保护的ZIP归档
        const archive = archiver.create('zip-encrypted', {
          zlib: { level: this.defaultCompressionLevel }, // 使用最高压缩级别
          encryptionMethod: 'zip20', // 使用传统Zip 2.0加密，兼容性更好
          password: password // 设置密码
        });
        
        // 连接输出流
        archive.pipe(output);
        
        // 处理流关闭事件（完成归档）
        output.on('close', () => {
          console.log(`创建加密归档完成：${destPath}`);
          // 计算文件大小
          const stats = fs.statSync(destPath);
          // 返回结果
          resolve({
            path: destPath,
            size: stats.size,
            iv: null // 传统加密不需要IV
          });
        });
        
        // 错误处理
        archive.on('error', (err) => {
          console.error('创建加密归档出错:', err);
          reject(new Error(`创建ZIP文件失败: ${err.message}`));
        });
        
        // 判断源路径是文件还是目录，采用不同的处理方式
        const stats = fs.statSync(sourcePath);
        if (stats.isDirectory()) {
          // 目录处理 - 添加整个目录到压缩包
          archive.directory(sourcePath, false);
        } else {
          // 单个文件处理 - 添加单个文件
          archive.file(sourcePath, { name: path.basename(sourcePath) });
        }
        
        // 完成归档
        archive.finalize();
        
      } catch (error) {
        console.error('加密归档过程出错:', error);
        reject(new Error(`备份过程出错: ${error.message}`));
      }
   
    });
  }
  
  /**
   * 生成随机密码并返回密码提示
   * 为每次备份创建唯一密码，并生成便于记忆的提示
   * @returns {Object} 包含密码和提示的对象
   */
  generatePassword() {
    // 生成8位随机密码（4字节的随机数转为16进制，得到8个字符）
    const password = crypto.randomBytes(4).toString('hex');
    
    // 创建密码提示（仅显示首尾字符，不泄露完整密码）
    const hint = `首字符: ${password.charAt(0)}，末字符: ${password.charAt(password.length - 1)}`;
    
    return { password, hint };
  }
  
  /**
   * 全量备份 - 优化大数据量处理
   * 执行完整数据库备份，适用于初次备份或周期性完整备份
   * 采用分片策略处理大体积学生试卷数据
   * @returns {Promise<Object>} 备份结果信息
   */
  async fullBackup() {
    // 创建备份记录，标记开始备份
    const backupId = await this._startBackupRecord('full');
  
    try {
      // 生成带时间戳的备份文件夹名称
      const timestamp = moment.tz('Asia/Shanghai').format('YYYY-MM-DD_HH-mm-ss');
      const backupFolder = path.join(this.fullBackupDir, `full_backup_${timestamp}`);
      fs.ensureDirSync(backupFolder); // 创建临时备份文件夹
      
      // 生成备份加密密码
      const password = 'SecurePassword123!';
      const hint = '密码提示';
      
      // 1. 备份系统表和基础数据（小体积）
      const systemTables = [
        'role', 'user', 'classes', 'subject', 'student_classes', 
        'teacher_classes_subject', 'examination', 'answer_type', 
        'question_type', 'big_question', 'knowledge_point', 
        'question', 'subjective_answers', 'objective_answers', 
        'exam_plan', 'exam_plan_details',
        // 添加之前缺少的表
        'review_assignment', 'reminder_log', 'reminder_setting',
        'recheck', 'recheck_detail', 'recheck_result',
        'system_notification', 'notification_type', 'question_analysis_report',
        'question_anomaly_detail'
      ];
      
      // 使用mysqldump导出系统表数据
      await mysqldump({
        connection: {
          host: process.env.DATABASE_HOST,
          user: process.env.DATABASE_USER,
          password: process.env.DATABASE_PASSWORD,
          database: process.env.DATABASE_NAME,
        },
        dumpToFile: path.join(backupFolder, 'system_tables.sql'), // 系统表备份文件
        dump: {
          tables: systemTables, // 指定要备份的表
        }
      });
      
      // 2. 分片备份大体积学生试卷数据
      // 获取学生试卷总数，用于确定分片数量
      const totalPapers = await prisma.student_paper.count();
      const totalSlices = Math.ceil(totalPapers / this.sliceSize); // 计算需要的分片数
      
      // 分批处理学生试卷和相关表
      for (let slice = 0; slice < totalSlices; slice++) {
        // 获取当前分片的学生试卷ID范围
        const paperIds = await prisma.student_paper.findMany({
          select: { id: true },
          skip: slice * this.sliceSize,   // 跳过前面已处理的记录
          take: this.sliceSize,           // 获取当前分片的记录数
          orderBy: { id: 'asc' }          // 按ID升序排序确保顺序一致
        });
        
        // 提取ID列表
        const paperIdList = paperIds.map(p => p.id);
        
        // 如果当前分片没有数据，跳过
        if (paperIdList.length === 0) continue;
        
        // 为每个分片创建专用SQL文件
        const sliceFile = path.join(backupFolder, `papers_slice_${slice + 1}.sql`);
        
        // 导出当前分片的学生试卷及相关数据
        await this._dumpPaperSlice(paperIdList, sliceFile);
      }
      
      // 3. 打包并加密整个备份文件夹
      const finalZipFile = `${backupFolder}.zip`; // 最终加密备份文件路径
      const archiveResult = await this.createEncryptedArchive(backupFolder, finalZipFile, password);
      
      // 备份完成后删除临时文件夹，只保留加密的zip文件
      fs.removeSync(backupFolder);
      
      // 计算备份文件大小
      const stats = fs.statSync(finalZipFile);
      const fileSizeInMB = (stats.size / (1024 * 1024)).toFixed(2); // 转换为MB并保留两位小数
      
      // 更新备份记录，标记备份完成
      await this._completeBackupRecord(backupId, {
        filename: path.basename(finalZipFile), // 文件名
        backup_path: finalZipFile,             // 完整路径
        encrypt_iv: archiveResult.iv,          // 加密IV
        backup_size: `${fileSizeInMB} MB`,     // 文件大小
        compress_type: 'encrypted',            // 压缩类型
        password_hint: hint,                   // 密码提示
        slice_count: totalSlices,              // 分片数量
        status: 'success',                     // 备份状态
        description: `加密全量备份，共${totalPapers}份试卷，分${totalSlices}个分片`, // 描述
      });
      
      console.log(`全量备份完成: ${finalZipFile}`); // 输出备份完成信息
      
      // 返回备份结果
      return {
        success: true,
        path: finalZipFile,
        size: `${fileSizeInMB} MB`,
        slices: totalSlices,
        passwordHint: hint
      };
    } catch (error) {
      // 备份过程出错处理
      console.error('全量备份失败:', error);
      
      // 更新备份记录为失败状态
      await this._completeBackupRecord(backupId, {
        status: 'failed',
        description: `备份失败: ${error.message}`
      });
      
      // 返回错误信息
      return {
        success: false,
        error: error.message
      };
    }
  }
  
  /**
   * 每日增量备份 - 只备份当天变更数据
   * 对上次备份后发生变化的数据进行备份，减少备份文件大小和执行时间
   * @returns {Promise<Object>} 备份结果信息
   */
  async incrementalBackup() {
     // 创建增量备份记录，标记开始备份
     const backupId = await this._startBackupRecord('incremental');
     const password = 'SecurePassword123!';
     let backupFolder; // 声明在外部以便错误处理时可以访问
     
     try {
       // 获取增量备份的时间范围（上次备份时间到现在）
       const { startTime, endTime, startTimeDate, endTimeDate } = await this.getIncrementalTimeRange();
       
       console.log('startTime: ', startTime);
       console.log('endTime: ', endTime);
  
       // 如果没有找到上次备份时间，无法执行增量备份
       if (!startTime) {
         // 更新备份记录为失败状态
         await this._completeBackupRecord(backupId, {
           status: 'failed',
           description: '没有找到之前的备份点，请先执行全量备份'
         });
         
         // 返回错误信息
         return {
           success: false,
           error: '没有找到之前的备份点，请先执行全量备份'
         };
       }
       
       // 统一使用东八区时间对象
       const beijingTime = moment.tz('Asia/Shanghai');
       const timestamp = beijingTime.format('YYYY-MM-DD_HH-mm-ss');
       backupFolder = path.join(this.incrementalBackupDir, `incremental_backup_${timestamp}`);
       fs.ensureDirSync(backupFolder); // 创建临时备份文件夹
 
       // 构建时间过滤条件
       const updatedTimeFilter = `updated_at >= '${startTime}' AND updated_at <= '${endTime}'`;
    
       console.log("startTime: ", startTime);
       console.log("endTime: ", endTime);
       console.log("startTimeDate: ", startTimeDate);
       console.log("endTimeDate: ", endTimeDate);
       console.log(`SQL筛选条件: ${updatedTimeFilter}`);
       
    
       
       // 记录备份数据范围描述（起止时间）
       const rangeDescription = `${startTime} 至 ${endTime}`;
       console.log('rangeDescription: ', rangeDescription);
 
       // 1. 按数据库表结构分组备份不同类型的表
       
       // 1.1 基础数据表
       const baseTables = [
         'role', 'user', 'classes', 'subject', 'student_classes', 
         'teacher_classes_subject', 'notification_type', 'reminder_setting'
       ];
       
       // 1.2 考试相关核心表
       const examTables = [
         'examination', 'exam_plan', 'exam_plan_details'
       ];
       
       // 1.3 试题相关表
       const questionTables = [
         'answer_type', 'question_type', 'big_question', 'knowledge_point',
         'question', 'subjective_answers', 'objective_answers'
       ];
       
       // 1.4 批阅相关表
       const reviewTables = [
         'review_assignment', 'review_assignment_detail'
       ];
       
       // 1.5 查卷复审相关表
       const recheckTables = [
         'recheck_request', 'recheck', 'recheck_detail', 'recheck_result'
       ];
       
       // 1.6 通知消息相关表
       const notificationTables = [
         'system_notification', 'reminder_log'
       ];
       
       // 1.7 学生试卷相关表
       const paperTables = [
         'student_paper', 'paper_detail'
       ];
       
       // 1.8 试题异常分析报告相关表
       const questionAnalysisTables = [
         'question_analysis_report', 'question_anomaly_detail'
       ];

       // 合并所有需要备份的表
       const allTablesToBackup = [
         ...baseTables, 
         ...examTables, 
         ...questionTables, 
         ...reviewTables, 
         ...recheckTables, 
         ...notificationTables,
         ...paperTables,
         ...questionAnalysisTables
       ];
       
       // 构建各表的筛选条件，确保每个表都应用时间过滤
       const tableConditions = {};
       allTablesToBackup.forEach(table => {
         tableConditions[table] = updatedTimeFilter;
       });
       
       console.log(`增量备份: 开始备份 ${allTablesToBackup.length} 个表的变更数据`);
       
       // 导出变更的表数据 - 使用 mysqldump 命令行工具直接执行，以确保过滤条件正确应用
       const dbConfig = {
         host: process.env.DATABASE_HOST,
         user: process.env.DATABASE_USER,
         password: process.env.DATABASE_PASSWORD,
         database: process.env.DATABASE_NAME
       };
       
      
       
       // 使用 mysqldump 命令行直接执行，确保过滤条件正确应用
       const { execSync } = require('child_process');
       const outputFile = path.join(backupFolder, 'modified_tables.sql');
       
       // 为每个表执行单独的导出，确保过滤条件应用
       let combinedSql = '';
       for (const table of allTablesToBackup) {
         try {
           const tableSqlFile = path.join(backupFolder, `${table}.sql`);
           
           // 构建mysqldump命令
           const dumpCmd = `mysqldump --host=${dbConfig.host} --user=${dbConfig.user} --password=${dbConfig.password} ` + 
                          `--no-create-info --skip-triggers --where="${tableConditions[table]}" ` +
                          `${dbConfig.database} ${table} > "${tableSqlFile}"`;
           
           console.log(`备份表 ${table} 中...`);
           execSync(dumpCmd, { stdio: 'pipe' });
           
           // 读取表SQL并合并
           if (fs.existsSync(tableSqlFile)) {
             const tableSql = fs.readFileSync(tableSqlFile, 'utf8');
             combinedSql += tableSql + '\n\n';
             // 删除单表SQL文件
             fs.unlinkSync(tableSqlFile);
           }
         } catch (err) {
           console.error(`表 ${table} 备份失败: ${err.message}`);
         }
       }
       
       // 写入合并后的SQL文件
       fs.writeFileSync(outputFile, combinedSql);
       
       console.log(`增量备份: 已完成所有表数据备份`);
       
       // 记录备份中涉及到的试卷数量统计
       const totalPapers = await prisma.student_paper.count({
         where: {
           updated_at: { 
             gte: new Date(startTime),
             lte: new Date(endTime)
           }
         }
       });
       
       console.log(`增量备份: 本次备份共包含 ${totalPapers} 份变更试卷`);
       
       // 3. 打包并加密整个备份文件夹
       console.log(`增量备份: 开始压缩加密备份数据`);
       const finalZipFile = `${backupFolder}.zip`; // 最终加密备份文件路径
       const archiveResult = await this.createEncryptedArchive(backupFolder, finalZipFile, password);
       
       // 备份完成后删除临时文件夹，只保留加密的zip文件
       try {
         await this._removeDirectoryRecursive(backupFolder);
       } catch (err) {
         console.warn('删除临时文件夹失败:', err);
       }
       
       // 计算备份文件大小
       const stats = fs.statSync(finalZipFile);
       const fileSizeInMB = (stats.size / (1024 * 1024)).toFixed(2); // 转换为MB并保留两位小数
       
       // 更新备份记录，标记备份完成
       await this._completeBackupRecord(backupId, {
         filename: path.basename(finalZipFile), // 文件名
         backup_path: finalZipFile,             // 完整路径
         encrypt_iv: archiveResult.iv,          // 加密IV
         backup_size: `${fileSizeInMB} MB`,     // 文件大小
         compress_type: 'encrypted',            // 压缩类型
         password_hint: '密码提示',                   // 密码提示
         data_range: rangeDescription,          // 数据时间范围
         slice_count: Math.ceil(totalPapers / this.sliceSize), // 分片数量
         status: 'success',                     // 备份状态
         description: `加密增量备份，${rangeDescription}期间的变更数据`, // 描述
       });
       
       console.log(`增量备份完成: ${finalZipFile}`); // 输出备份完成信息
       
       // 返回备份结果
       return {
         success: true,
         path: finalZipFile,
         size: `${fileSizeInMB} MB`,
         range: rangeDescription,
         paperCount: totalPapers,
         tableCount: allTablesToBackup.length,
         passwordHint: '密码提示'
       };
     } catch (error) {
       // 增量备份过程出错处理
       console.error('增量备份失败:', error);
       
       // 更新备份记录为失败状态
       await this._completeBackupRecord(backupId, {
         status: 'failed',
         description: `增量备份失败: ${error.message}`
       });
       
       // 返回错误信息
       return {
         success: false,
         error: error.message
       };
     }
  }
  
  /**
   * 导出指定ID范围的学生试卷及相关数据
   * 将指定试卷ID列表相关的所有数据导出为SQL文件
   * 包括试卷、试卷详情、批阅任务详情、查卷申请和复审数据
   * @param {Array<number>} paperIds 试卷ID数组
   * @param {string} outputFile 输出SQL文件路径
   * @returns {Promise<boolean>} 操作成功返回true
   */
  async _dumpPaperSlice(paperIds, outputFile) {
    // 验证参数有效性
    if (!paperIds || paperIds.length === 0) return;
    
    // 创建临时SQL文件，使用.tmp后缀避免因中断操作导致文件损坏
    const tempSqlFile = outputFile + '.tmp';
    const writeStream = fs.createWriteStream(tempSqlFile);
    
    try {
      // 添加SQL文件头部注释，记录导出时间和数据范围
      writeStream.write(`-- 导出时间: ${new Date().toISOString()}\n`);
      writeStream.write(`-- 试卷ID范围: ${Math.min(...paperIds)} - ${Math.max(...paperIds)}\n\n`);
      
      // 1. 导出学生试卷表数据
      const paperData = await prisma.student_paper.findMany({
        where: { id: { in: paperIds } } // 筛选指定ID的试卷
      });
      
      // 如果找到试卷数据，生成INSERT语句
      if (paperData.length > 0) {
        writeStream.write(`-- 学生试卷表数据\n`);
        writeStream.write(`INSERT INTO student_paper VALUES\n`);
        
        // 逐条生成试卷数据的SQL值部分
        paperData.forEach((paper, index) => {
          const values = this._formatSqlValues(paper);
          // 最后一条不加逗号，否则加逗号分隔
          writeStream.write(`${values}${index < paperData.length - 1 ? ',' : ';'}\n`);
        });
        
        writeStream.write(`\n`); // 添加空行分隔
      }
      
      // 2. 导出试卷详情表数据
      const detailData = await prisma.paper_detail.findMany({
        where: { student_paper_id: { in: paperIds } } // 筛选关联的试卷详情
      });
      
      // 如果找到试卷详情数据，生成INSERT语句
      if (detailData.length > 0) {
        writeStream.write(`-- 试卷详情表数据\n`);
        writeStream.write(`INSERT INTO paper_detail VALUES\n`);
        
        // 逐条生成试卷详情数据的SQL值部分
        detailData.forEach((detail, index) => {
          const values = this._formatSqlValues(detail);
          writeStream.write(`${values}${index < detailData.length - 1 ? ',' : ';'}\n`);
        });
        
        writeStream.write(`\n`); // 添加空行分隔
        
        // 保存所有试卷详情ID，用于后续查询复审详情
        const paperDetailIds = detailData.map(d => d.id);
      }
      
      // 3. 导出批阅任务详情数据
      const reviewDetailData = await prisma.review_assignment_detail.findMany({
        where: { student_paper_id: { in: paperIds } } // 筛选关联的批阅任务详情
      });
      
      // 如果找到批阅任务详情数据，生成INSERT语句
      if (reviewDetailData.length > 0) {
        writeStream.write(`-- 批阅任务详情表数据\n`);
        writeStream.write(`INSERT INTO review_assignment_detail VALUES\n`);
        
        // 逐条生成批阅任务详情的SQL值部分
        reviewDetailData.forEach((detail, index) => {
          const values = this._formatSqlValues(detail);
          writeStream.write(`${values}${index < reviewDetailData.length - 1 ? ',' : ';'}\n`);
        });
        
        writeStream.write(`\n`); // 添加空行分隔
      }
      
      // 4. 导出查卷申请数据
      const recheckRequestData = await prisma.recheck_request.findMany({
        where: { student_paper_id: { in: paperIds } } // 筛选关联的查卷申请
      });
      
      // 如果找到查卷申请数据，生成INSERT语句
      if (recheckRequestData.length > 0) {
        writeStream.write(`-- 查卷申请表数据\n`);
        writeStream.write(`INSERT INTO recheck_request VALUES\n`);
        
        // 逐条生成查卷申请数据的SQL值部分
        recheckRequestData.forEach((request, index) => {
          const values = this._formatSqlValues(request);
          writeStream.write(`${values}${index < recheckRequestData.length - 1 ? ',' : ';'}\n`);
        });
        
        writeStream.write(`\n`); // 添加空行分隔
        
        // 保存所有查卷申请ID，用于后续查询复审表
        const recheckRequestIds = recheckRequestData.map(r => r.id);
        
        // 5. 导出复审表数据
        if (recheckRequestIds.length > 0) {
          const recheckData = await prisma.recheck.findMany({
            where: { recheck_request_id: { in: recheckRequestIds } } // 筛选关联的复审记录
          });
          
          if (recheckData.length > 0) {
            writeStream.write(`-- 复审表数据\n`);
            writeStream.write(`INSERT INTO recheck VALUES\n`);
            
            recheckData.forEach((recheck, index) => {
              const values = this._formatSqlValues(recheck);
              writeStream.write(`${values}${index < recheckData.length - 1 ? ',' : ';'}\n`);
            });
            
            writeStream.write(`\n`); // 添加空行分隔
            
            // 保存所有复审ID，用于后续查询复审详情
            const recheckIds = recheckData.map(r => r.id);
            
            // 6. 导出复审详情表数据
            if (recheckIds.length > 0) {
              const recheckDetailData = await prisma.recheck_detail.findMany({
                where: { 
                  OR: [
                    { recheck_id: { in: recheckIds } },       // 关联到复审表
                    { student_paper_id: { in: paperIds } }    // 或直接关联到学生试卷
                  ]
                }
              });
              
              if (recheckDetailData.length > 0) {
                writeStream.write(`-- 复审详情表数据\n`);
                writeStream.write(`INSERT INTO recheck_detail VALUES\n`);
                
                recheckDetailData.forEach((detail, index) => {
                  const values = this._formatSqlValues(detail);
                  writeStream.write(`${values}${index < recheckDetailData.length - 1 ? ',' : ';'}\n`);
                });
                
                writeStream.write(`\n`); // 添加空行分隔
              }
            }
            
            // 7. 导出查卷结果表数据
            const recheckResultData = await prisma.recheck_result.findMany({
              where: { recheck_request_id: { in: recheckRequestIds } } // 筛选关联的查卷结果
            });
            
            if (recheckResultData.length > 0) {
              writeStream.write(`-- 查卷结果表数据\n`);
              writeStream.write(`INSERT INTO recheck_result VALUES\n`);
              
              recheckResultData.forEach((result, index) => {
                const values = this._formatSqlValues(result);
                writeStream.write(`${values}${index < recheckResultData.length - 1 ? ',' : ';'}\n`);
              });
              
              writeStream.write(`\n`); // 添加空行分隔
            }
          }
        }
      }
      
      // 关闭写入流，确保所有数据写入文件
      writeStream.end();
      
      // 完成后重命名临时文件为最终文件名，确保文件完整性
      await fs.rename(tempSqlFile, outputFile);
      
      return true; // 操作成功
    } catch (error) {
      // 导出过程出错处理
      console.error('导出试卷分片失败:', error);
      
      // 关闭写入流
      writeStream.end();
      
      // 清理临时文件，避免留下损坏的文件
      if (fs.existsSync(tempSqlFile)) {
        fs.unlinkSync(tempSqlFile);
      }
      
      // 抛出错误，由上层函数处理
      throw error;
    }
  }
  
  /**
   * 将对象格式化为SQL值字符串
   * 处理不同数据类型，确保生成有效的SQL值语法
   * @param {Object} obj 包含字段值的对象
   * @returns {string} 格式化后的SQL VALUES语句部分
   */
  _formatSqlValues(obj) {
    // 将对象的值转换为SQL兼容的字符串表示
    const values = Object.values(obj).map(value => {
      if (value === null) return 'NULL'; // NULL值处理
      if (value instanceof Date) return `'${value.toISOString().slice(0, 19).replace('T', ' ')}'`; // 日期格式化
      if (typeof value === 'string') return `'${value.replace(/'/g, "''")}'`; // 字符串处理，转义单引号
      return value; // 其他类型直接返回
    });
    
    // 组合为(value1, value2, ...)格式
    return `(${values.join(', ')})`;
  }
  
  /**
   * 开始一个备份记录
   * 在数据库中创建备份任务记录，标记备份开始
   * @param {string} backupType 备份类型（'full', 'incremental', 'differential'）
   * @returns {Promise<number>} 新创建的备份记录ID
   */
  async _startBackupRecord(backupType) {
    const now = new Date();
    const utc8Date = new Date(now.getTime() + (8 * 60 * 60 * 1000));
    // 在数据库中创建一条备份记录，标记为进行中状态
    const record = await prisma.backup_record.create({
      data: {
        filename: 'pending',          // 临时文件名，将在完成后更新
        backup_path: 'pending',       // 临时路径，将在完成后更新
        backup_type: backupType,      // 备份类型
        start_time: utc8Date,      // 备份开始时间（东八区）
        end_time: utc8Date,        // 临时结束时间（东八区）
        status: 'in_progress',        // 状态：进行中
        compress_type: 'encrypted',   // 添加压缩类型字段
        description: `${backupType} 备份进行中...`, // 初始描述
      }
    });
    
    return record.id; // 返回新创建记录的ID
  }
  
  /**
   * 完成一个备份记录
   * 更新数据库中的备份记录，标记备份完成或失败
   * @param {number} backupId 备份记录ID
   * @param {Object} data 要更新的数据
   * @returns {Promise<void>}
   */
  async _completeBackupRecord(backupId, data) {
    // 更新备份记录，包含最终状态和结束时间
    const now = new Date();
    const utc8Date = new Date(now.getTime() + (8 * 60 * 60 * 1000));
    await prisma.backup_record.update({
      where: { id: backupId },
      data: {
      ...data,                // 更新传入的所有数据
      end_time: utc8Date,   // 设置备份结束时间（东八区）
      created_at: utc8Date
    }
    });
  }
  
  /**
   * 清理过期备份
   * 删除指定天数前的备份文件和数据库记录，但保留最近的全量备份
   * @param {number} days 保留的天数，默认30天
   * @returns {Promise<Object>} 清理结果信息
   */
  async cleanOldBackups(days = 30) {
    try {
      // 计算截止日期（当前日期减去指定天数）
      const cutoffMoment = moment.tz('Asia/Shanghai')
      .subtract(days, 'days');
      const cutoffDate = cutoffMoment.format('YYYY-MM-DD HH:mm:ss');
      console.log("cutoffDate: ",cutoffDate);  
      
      // 找到最近的成功全量备份，这个备份将被保留
      const lastFullBackup = await prisma.backup_record.findFirst({
        where: {
          backup_type: 'full',
          status: 'success'
        },
        orderBy: {
          created_at: 'desc'
        }
      });

 
      
      // 查询过期的备份记录
      const oldBackups = await prisma.backup_record.findMany({
        where: {
          updated_at: {
            lt: new Date(cutoffDate) // 早于截止日期的记录
          },
          status: 'success',
          // 排除最近的全量备份（如果存在）
          NOT: lastFullBackup ? {
            id: lastFullBackup.id
          } : undefined
        }
      });

 
      
      // 删除文件并更新数据库
      for (const backup of oldBackups) {
        // 删除备份文件
        if (fs.existsSync(backup.backup_path)) {
          fs.unlinkSync(backup.backup_path);
        }
        
        // 删除数据库记录
        await prisma.backup_record.delete({
          where: { id: backup.id }
        });
      }
      
      console.log(`已清理 ${oldBackups.length} 个过期备份`);
      return {
        success: true,
        count: oldBackups.length // 返回清理的备份数量
      };
    } catch (error) {
      console.error('清理过期备份失败:', error);
      return {
        success: false,
        error: error.message
      };
    }
  }
  
  /**
   * 获取备份列表
   * 查询数据库中的备份记录，支持分页和按类型筛选
   * @param {string|null} type 备份类型筛选，null表示所有类型
   * @param {number} limit 每页记录数，默认50
   * @param {number} offset 跳过的记录数，用于分页
   * @param {string} dataYear 备份数据的年份筛选，为空字符串表示不筛选
   * @returns {Promise<Object>} 备份列表和总数
   */
  async getBackupList(type = null, limit = 50, offset = 0, dataYear = '') {
    try {
      // 构建查询条件
      let where = {
        // 过滤掉filename为'pending'的记录
        filename: {
          not: 'pending'
        },
        // 只显示full和incremental类型的备份
        backup_type: {
          in: ['full', 'incremental']
        }
      };
      
      // 如果指定了类型则按类型筛选
      if (type) {
        where.backup_type = type;
      }
    
      // 如果指定了年份则按年份筛选
      if (dataYear && dataYear !== '') {
        where.created_at = {
          gte: new Date(Date.UTC(dataYear, 0, 1)),       // 2025-01-01T00:00:00.000Z
          lte: new Date(Date.UTC(dataYear, 11, 31, 23, 59, 59, 999))  // 2025-12-31T23:59:59.999Z
        };
      }
      
      // 查询备份记录，按创建时间倒序排列
      const backups = await prisma.backup_record.findMany({
        where,
        orderBy: {
          created_at: 'desc' // 最新的备份排在前面
        },
        skip: offset,        // 分页参数：跳过记录数
        take: limit          // 分页参数：获取记录数
      });
      
      // 获取符合条件的总记录数
      const count = await prisma.backup_record.count({ where });
      
      const formatDateTimeUTC = (dateString) => {
        if (!dateString) return null;
        const date = new Date(dateString);
        
        // 直接提取 UTC 时间部分，避免时区问题
        const year = date.getUTCFullYear();
        const month = String(date.getUTCMonth() + 1).padStart(2, '0');
        const day = String(date.getUTCDate()).padStart(2, '0');
        const hours = String(date.getUTCHours()).padStart(2, '0');
        const minutes = String(date.getUTCMinutes()).padStart(2, '0');
        const seconds = String(date.getUTCSeconds()).padStart(2, '0');
        
        return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
      };

      // 将所有属性名转换为小驼峰格式，并格式化日期
      const formattedBackups = backups.map(backup => {
        
        
        
        return {
          id: backup.id,
          filename: backup.filename,
          backupPath: backup.backup_path,
          backupType: backup.backup_type,
          backupSize: backup.backup_size,
          compressType: backup.compress_type,
          passwordHint: backup.password_hint,
          startTime: formatDateTimeUTC(backup.start_time),
          endTime: formatDateTimeUTC(backup.end_time),
          createdAt: formatDateTimeUTC(backup.created_at),
          status: backup.status,
          dataRange: backup.data_range,
          sliceCount: backup.slice_count,
          description: backup.description,
          encryptIv: backup.encrypt_iv
        };
      });
      
      // 返回查询结果
      return {
        success: true,
        data: formattedBackups,  // 格式化后的备份记录列表
        total: count    // 总记录数
      };
    } catch (error) {
      console.error('获取备份列表失败:', error);
      return {
        success: false,
        error: error.message
      };
    }
  }


/**
 * 还原备份数据到数据库，解决主键冲突问题
 * @param {number} backupId 要还原的备份记录ID
 * @param {string} password 解密备份文件的密码
 * @param {Object} options 还原选项，包含处理冲突的策略
 * @returns {Promise<Object>} 还原结果信息
 */
async restoreBackup(backupId, password='SecurePassword123!', options = {}) {
  // 默认选项设置
  const defaultOptions = {
    conflictStrategy: 'replace', // 冲突策略: replace(替换), truncate(先清空), ignore(忽略)
    backupBeforeRestore: false,   // 是否在还原前先备份当前数据
    ignoreErrors: false          // 是否忽略部分错误继续执行
  };
  // 合并用户提供的选项
  const restoreOptions = {...defaultOptions, ...options};
  backupId = parseInt(backupId);
  
  // 获取备份记录详情
  const backupRecord = await prisma.backup_record.findUnique({
        where: { id: backupId }
  });

  
  // 处理解压和恢复的临时目录与文件
  const timestamp = moment.tz('Asia/Shanghai').format('YYYY-MM-DD_HH-mm-ss');
  const tempDir = path.join(this.backupDir, `temp_${timestamp}`);
  const extractDir = path.join(tempDir, 'extract');
  
  
  let fileSizeInMB = '未知';
  let importedFiles = 0;

  try{
    // 1. 解密并解压备份文件
    let sqlFiles = [];

    const AdmZip = require('adm-zip');
    const zip = new AdmZip(backupRecord.backup_path);
    
    // 尝试读取ZIP文件头
    const zipEntries = zip.getEntries();
    console.log(`ZIP文件包含 ${zipEntries.length} 个条目`);


    // 检查是否是加密的zip文件
    const isEncrypted = zipEntries.some(entry => entry.isEncrypted);
    console.log("isEncrypted: ",isEncrypted);
    if (password) {
        console.log('尝试用密码解压（即使文件未标记为加密）');
        zip.extractAllTo(extractDir, true, false, password);
    } else {
        console.log('未提供密码，尝试直接解压');
        zip.extractAllTo(extractDir, true);
    }
      
    // 验证解压结果
    const extractedFiles = fs.readdirSync(extractDir);
    console.log(`解压出 ${extractedFiles.length} 个文件`);

    // 验证解压出的文件
    sqlFiles = extractedFiles.filter(file => file.toLowerCase().endsWith('.sql'));
    if (sqlFiles.length === 0) {
      throw new Error('解压后未找到SQL文件，备份文件可能已损坏');
    }

    // 检查SQL文件的有效性
    for (const sqlFile of sqlFiles) {
      const sqlFilePath = path.join(extractDir, sqlFile);
      const sqlContent = await fs.readFile(sqlFilePath, 'utf8');
      
      if (!sqlContent || sqlContent.trim().length === 0) {
        throw new Error(`SQL文件 ${sqlFile} 为空`);
      }
      
      // 简单验证SQL文件格式
      if (!sqlContent.toLowerCase().includes('insert into') && 
          !sqlContent.toLowerCase().includes('create table')) {
        throw new Error(`SQL文件 ${sqlFile} 不包含有效的SQL语句`);
      }
    }
    
    console.log('备份文件解密和验证成功，准备开始还原数据...');

    // 6. 如果选择还原前备份，先进行一次备份
    if (restoreOptions.backupBeforeRestore) {
      try {
        console.log('在还原前进行数据备份...');
        const backupResult = await this.fullBackup(password);
        if (!backupResult.success) {
          throw new Error(backupResult.error || '备份失败');
        }
        console.log(`还原前的备份已完成: ${backupResult.path}`);
      } catch (backupError) {
        throw new Error(`还原前的备份失败: ${backupError.message}`);
      }
    }

    // 继续执行后续的还原步骤...
    // 7. 根据还原策略处理SQL文件
    await this._preprocessSqlFiles(extractDir, restoreOptions.conflictStrategy);

    // 8. 获取数据库连接信息
    const dbConfig = {
      host: process.env.DATABASE_HOST,
      user: process.env.DATABASE_USER,
      password: process.env.DATABASE_PASSWORD,
      database: process.env.DATABASE_NAME
    };
    
    console.log(`准备使用 ${restoreOptions.conflictStrategy} 策略还原数据到数据库: ${dbConfig.database}`);
    
    // 9. 使用子进程执行mysql命令导入SQL文件
    const { execSync } = require('child_process');
    
    // 按优先级排序SQL文件
    sqlFiles.sort((a, b) => {
      // 优先级排序：系统表 > 变更表 > 分片文件
      if (a === 'system_tables.sql') return -1;
      if (b === 'system_tables.sql') return 1;
      if (a === 'modified_tables.sql') return -1;
      if (b === 'modified_tables.sql') return 1;
      if (a.includes('papers_slice_') && !b.includes('papers_slice_')) return 1;
      if (!a.includes('papers_slice_') && b.includes('papers_slice_')) return -1;
      return a.localeCompare(b); // 保持稳定排序
    });
    
    // 10. 执行SQL文件导入
    console.log(`开始按顺序导入 ${sqlFiles.length} 个SQL文件...`);
    const importErrors = [];
    
    for (const sqlFile of sqlFiles) {
      const sqlFilePath = path.join(extractDir, sqlFile);
      console.log(`正在导入: ${sqlFile}`);
      
      try {
        // 构建mysql导入命令
        const importCmd = `mysql -h${dbConfig.host} -u${dbConfig.user} -p${dbConfig.password} ${dbConfig.database} < "${sqlFilePath}"`;
        
        // 执行导入命令
        execSync(importCmd, { stdio: 'pipe' });
        console.log(`- ${sqlFile} 导入成功`);
        importedFiles++;
      } catch (error) {
        console.error(`- ${sqlFile} 导入失败: ${error.message}`);
        importErrors.push(`${sqlFile}: ${error.message}`);
        
        // 如果不忽略错误，则终止还原过程
        if (!restoreOptions.ignoreErrors) {
          throw new Error(`SQL导入失败(${sqlFile}): ${error.message}`);
        }
      }
    }

    console.log(`数据恢复完成！共导入 ${importedFiles}/${sqlFiles.length} 个文件`);

     // 删除所有临时文件和目录
     try {
      console.log('清理所有临时目录...');
      if (fs.existsSync(tempDir)) {
        await fs.remove(tempDir);
        console.log(`- 已删除临时目录: ${tempDir}`);
      }
    } catch (cleanupError) {
      console.warn('清理临时目录失败:', cleanupError);
    }

    return {
      code: 200,
      message: `成功从上传文件(${backupRecord.filename})恢复数据`,
      strategy: restoreOptions.conflictStrategy,
      restoredFiles: importedFiles,
      totalFiles: sqlFiles.length,
      errors: importErrors.length > 0 ? importErrors : undefined
    };

  }catch(error){
    console.error('还原备份失败:', error);

    // 清理临时文件
    try {
      console.log('由于发生错误，清理所有临时文件...');
      if (fs.existsSync(tempDir)) {
        await fs.remove(tempDir);
        console.log(`- 已删除临时目录: ${tempDir}`);
      }
    } catch (cleanupError) {
      console.warn('清理临时文件失败:', cleanupError);
    }

    return {
        code: 500,
        message: `还原备份失败: ${error.message}`,
        data: null
    };
  }



 
}



/**
 * 解密并解压备份文件
 * 使用加密时相同的密钥和算法解密ZIP文件
 * @param {string} backupPath 备份文件路径
 * @param {string} extractDir 解压目标目录
 * @param {string} password 解密密码
 * @param {string} ivHex 加密使用的初始化向量(十六进制字符串)
 * @returns {Promise<void>}
 */
async _decryptAndExtractBackup(backupPath, extractDir, password, ivHex) {
  return new Promise((resolve, reject) => {
    try {
      console.log('开始解密备份文件...');
      
      // 创建临时解密文件
      const decryptedZipPath = path.join(extractDir, 'decrypted.zip');
      const outputFile = fs.createWriteStream(decryptedZipPath);
      
      // 读取加密的ZIP文件
      const inputFile = fs.createReadStream(backupPath);
      
      // 从十六进制字符串转换为Buffer对象
      let iv;
      if (ivHex) {
        // 使用备份记录中存储的IV
        iv = Buffer.from(ivHex, 'hex');
        console.log(`使用存储的IV进行解密: ${ivHex.substring(0, 8)}...`);
      } else {
        // 如果IV不存在(老备份文件)，使用零填充的IV(不推荐，但作为兼容处理)
        console.warn('警告: 备份记录中没有IV信息，使用零填充IV解密可能会失败');
        iv = Buffer.alloc(16, 0);
      }
      
      // 创建解密器
      const decipher = crypto.createDecipheriv(
        this.defaultEncryptionAlgorithm,
        crypto.scryptSync(password, 'salt', 32), // 使用与加密相同的密钥生成方法
        iv // 使用正确的IV进行解密
      );
      
      // 解密流程完成处理
      outputFile.on('finish', async () => {
        console.log('文件解密完成，开始解压...');
        
        try {
          // 使用unzipper解压文件
          const unzipStream = fs.createReadStream(decryptedZipPath)
            .pipe(require('unzipper').Extract({ path: extractDir }));
          
          // 监听解压完成事件
          unzipStream.on('close', () => {
            console.log('文件解压完成');
            
            // 删除临时解密ZIP文件
            fs.unlinkSync(decryptedZipPath);
            
            resolve();
          });
          
          unzipStream.on('error', (err) => {
            console.error('解压过程出错:', err);
            reject(new Error(`解压失败: ${err.message}`));
          });
        } catch (extractError) {
          console.error('解压初始化失败:', extractError);
          reject(new Error(`解压初始化失败: ${extractError.message}`));
        }
      });
      
      
      
      // 执行解密流程
      inputFile.pipe(decipher).pipe(outputFile);
      
    } catch (error) {
      console.error('解密过程出错:', error);
      reject(new Error(`解密过程出错: ${error.message}`));
    }
  });
}



/**
 * 处理备份SQL文件，解决主键冲突问题
 * @param {string} extractDir 解压目录
 * @param {string} conflictStrategy 冲突处理策略
 * @returns {Promise<void>}
 */
async _preprocessSqlFiles(extractDir, conflictStrategy) {
  console.log(`使用 ${conflictStrategy} 策略预处理SQL文件...`);
  
  // 获取所有SQL文件
  const sqlFiles = fs.readdirSync(extractDir).filter(file => file.endsWith('.sql'));
  
  for (const sqlFile of sqlFiles) {
    const filePath = path.join(extractDir, sqlFile);
    let sqlContent = fs.readFileSync(filePath, 'utf8');
    let modified = false;
    
    // 根据冲突策略修改SQL内容
    switch (conflictStrategy) {
      case 'replace':
        // 替换 INSERT INTO 为 REPLACE INTO
        const originalContent = sqlContent;
        sqlContent = sqlContent.replace(/INSERT INTO/gi, 'REPLACE INTO');
        modified = originalContent !== sqlContent;
        break;
        
      case 'ignore':
        // 替换 INSERT INTO 为 INSERT IGNORE INTO
        const beforeIgnore = sqlContent;
        sqlContent = sqlContent.replace(/INSERT INTO/gi, 'INSERT IGNORE INTO');
        modified = beforeIgnore !== sqlContent;
        break;
        
      case 'truncate':
        // 提取表名，为每个表添加TRUNCATE语句
        const tableNames = new Set();
        
        // 查找所有INSERT语句的表名
        const tableRegex = /INSERT INTO\s+[`"]?(\w+)[`"]?/gi;
        let match;
        while ((match = tableRegex.exec(sqlContent)) !== null) {
          tableNames.add(match[1]);
        }
        
        if (tableNames.size > 0) {
          // 创建TRUNCATE语句
          const truncateStatements = Array.from(tableNames)
            .map(table => `TRUNCATE TABLE \`${table}\`;`)
            .join('\n');
          
          // 在文件开头添加TRUNCATE语句
          sqlContent = truncateStatements + '\n\n' + sqlContent;
          modified = true;
        }
        break;
    }
    
    // 如果内容被修改，写回文件
    if (modified) {
      fs.writeFileSync(filePath, sqlContent, 'utf8');
      console.log(`- 已处理 ${sqlFile}`);
    }
  }
  
  console.log(`SQL文件预处理完成`);
}


/**
 * 递归删除目录及其内容
 * @param {string} dirPath 要删除的目录路径
 */
async _removeDirectoryRecursive(dirPath) {
  try {
    if (!fs.existsSync(dirPath)) return;
    
    // 使用fs-extra的remove方法，它能够处理非空目录
    await fs.remove(dirPath);
    console.log(`成功删除目录: ${dirPath}`);
  } catch (error) {
    console.warn(`删除目录失败: ${dirPath}`, error);
    throw error;
  }
}

}









module.exports = new BackupService();